#include "vanila/virtualmachine.h"
#include "vanila/opcode.h"
#include "vanila/chunk.h"
#include "vanila/disassembler.h"
#include "vanila/scanner.h"
#include "vanila/compiler.h"
#include "vanila/environment.h"
#include "vanila/object.h"
#include "vanila/callframe.h"
#include "vanila/error.h"
#include "vanila/native.h"
#include "vanila/allocator.h"
#include "utils/hash.h"
#include "utils/format.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>

namespace vanila
{
static Allocator* allocator = utils::Singleton<Allocator>::instance();
static Compiler* compiler = utils::Singleton<Compiler>::instance();

//! \brief Construct a new Virtual Machine:: Virtual Machine object
VirtualMachine::VirtualMachine() noexcept : 
    _frames{}, 
    _currentFrame{nullptr},
    _stack{}, 
    _stackTop{nullptr}, 
    _objects{},
    _strings{}, 
    _openUpvalues{nullptr},
    _globals{},
    _showStack{false},
    _showBytecodes{false}
{
}

//! \brief initial the virtual machine's resource
void VirtualMachine::init() noexcept
{
    ObjectClass::initMethodName = allocator->allocateString("__init__", 8);
    ObjectClass::strMethodName = allocator->allocateString("__str__", 7);
    ObjectClass::hashMethodName = allocator->allocateString("__hash__", 8);
    ObjectClass::equalMethodName = allocator->allocateString("__equal__", 9);
    ObjectClass::lessMethodName = allocator->allocateString("__less__", 8);
    ObjectClass::greaterMethodName = allocator->allocateString("__greater__", 11);
    ObjectClass::addMethodName = allocator->allocateString("__add__", 7);
    ObjectClass::subMethodName = allocator->allocateString("__sub__", 7);
    ObjectClass::mulMethodName = allocator->allocateString("__mul__", 7);
    ObjectClass::divMethodName = allocator->allocateString("__div__", 7);
}

//! \brief free the virtual machine
void VirtualMachine::release() noexcept
{
    for (Object* object : this->_objects)
        allocator->deallocateObject(object);
}

//! \brief register native function
void VirtualMachine::registerNativeFunctions()
{
    this->resetStack();
    this->defineNative("clock", Native::System::clock);
    this->defineNative("show_stack", Native::System::showStack);
    this->defineNative("show_bytecodes", Native::System::showBytecodes);
    this->defineNative("print", Native::System::print);
    this->defineNative("hash", Native::System::hash);
}

//! \brief register native class
void VirtualMachine::registerNativeClasses()
{
    // list
    ObjectString* listName = allocator->allocateString("list", 4);
    ObjectClass* listClass = allocator->allocateClass(listName, true);

    listClass->addInitMethod(Native::DataStructure::listInit);
    listClass->addMethod("append", Native::DataStructure::listAppend);
    listClass->addMethod("insert", Native::DataStructure::listInsert);
    listClass->addMethod("pop", Native::DataStructure::listPop);

    this->_globals[listName] = Value(listClass);
    ObjectClass::list = listClass;

    // dict
    ObjectString* dictName = allocator->allocateString("dict", 4);
    ObjectClass* dictClass = allocator->allocateClass(dictName, true);

    dictClass->addInitMethod(Native::DataStructure::dictInit);
    dictClass->addMethod("add", Native::DataStructure::dictAdd);
    dictClass->addMethod("get", Native::DataStructure::dictGet);

    this->_globals[dictName] = Value(dictClass);
    ObjectClass::dict = dictClass;

    // set
    ObjectString* setName = allocator->allocateString("set", 3);
    ObjectClass* setClass = allocator->allocateClass(setName, true);

    setClass->addInitMethod(Native::DataStructure::setInit);
    setClass->addMethod("add", Native::DataStructure::setAdd);
    setClass->addMethod("contain", Native::DataStructure::setContain);

    this->_globals[setName] = Value(setClass);
    ObjectClass::set = setClass;
}

//! \brief interpret the chunk's bytecode
//! \param[in] source the source code
void VirtualMachine::interpret(const std::string& source)
{
    this->resetStack();

    // compile the source to a function
    compiler->init(source.c_str());
    ObjectFunction* function = compiler->compile();
    
    if (function == nullptr)
        throw CompileError();

    this->push(Value(function));
    ObjectClosure* closure = allocator->allocateClosure(function);
    this->pop();
    this->push(Value(closure));

    this->call(closure, 0);

    this->run();
}

//! \brief a runtime error happend, throw a RuntimeError 
//! \param[in] message error message to show
void VirtualMachine::runtimeError(const std::string& message)
{
    std::cout << message << '\n';

    for (const CallFrame& frame : this->_frames)
    {
        ObjectFunction* function = frame.closure->function();
        uint32_t instruction = frame.ip - function->chunk().data() - 1;
        std::cout << "[line " << function->chunk().lines()[instruction] << "] in ";

        if (function->name() == nullptr)
            std::cout << "script\n";
        else
            std::cout << function->name()->str() << "()\n";
    }

    this->resetStack();
    throw RuntimeError();
}

//! \brief find specify hashvalue in string tables
//! \param[in] hash the hash value
//! \return ObjectString* if no foune, return nullptr
ObjectString* VirtualMachine::findString(uint32_t hash) const
{
    auto iter = this->_strings.find(hash);
    if (iter != this->_strings.end())
        return iter->second;
    return nullptr;
}

//! \brief find specfty string
//! \param[in] chars string ptr
//! \param[in] length string size
//! \return ObjectString* if no foune, return nullptr
ObjectString* VirtualMachine::findString(const char* chars, uint32_t length) const
{
    uint32_t hash = utils::fnv1aHash(chars, length);
    return this->findString(hash);
}

//! \brief execute the instance's method
//! \param[in] instance specify instance
//! \param[in] method specify method
//! \note the function do not check wheather the method is a instance's method, after call this,
//! \note the method's return value will be leave in stack top!
Value VirtualMachine::callIntanceMethod(const ObjectInstance* instance, const Value& method, const std::vector<Value>& arguments)
{    
    // push the instance
    this->push(Value( const_cast<ObjectInstance*>(instance)));

    // push the arguments
    for (const Value& argument : arguments)
        this->push(argument);

    this->call(method.asClosure(), arguments.size());
    this->run(this->_frames.size() - 1);

    return this->pop();
}

//! \brief reset the vm's stack
void VirtualMachine::resetStack() noexcept
{
    this->_stackTop = this->_stack;
    this->_frames.clear();
}

//! \brief print stack
void VirtualMachine::printStack()
{
    std::cout << "          ";
    for (Value* slot = this->_stack; slot < this->_stackTop; ++slot)
    {
        std::cout << "[ ";
        slot->print(false);
        std::cout << " ]";
    }
    std::cout << '\n';
}

//! \brief read the short value in stack top
uint16_t VirtualMachine::readShort() noexcept
{
    this->_currentFrame->ip += 2;
    return static_cast<uint16_t>( static_cast<uint8_t>(this->_currentFrame->ip[-2]) << 8 | static_cast<uint8_t>(this->_currentFrame->ip[-1]));
}

//! \brief operate the two operand at stack top
//! \param[in] oper operator object
//! \return Value
Value VirtualMachine::binaryOperate(OpCode operatorType)
{
    // check type
    Value y = this->pop();
    Value x = this->pop();
    
    switch (operatorType)
    {
    case OpCode::EQUAL:     return x.equal(y);
    case OpCode::GREATER:   return x.greater(y);
    case OpCode::LESS:      return x.less(y);
    case OpCode::ADD:       return x.add(y);
    case OpCode::SUBTRACT:  return x.sub(y);
    case OpCode::MULTIPLY:  return x.mul(y);
    case OpCode::DIVIDE:    return x.div(y);
    }

    return Value(ValueType::NIL);
}

//! \brief use the suitable way to call a calle value incroding to it's object type
bool VirtualMachine::callValue(Value callee, int argCount)
{
    if (callee.isObject())
    {
        switch (callee.objectType())
        {
        case ObjectType::BOUND_METHOD:
        {
            ObjectBoundMethod* bound = callee.asBoundMethod();
            // put 'this' in the first slot as a local variable
            this->_stackTop[-argCount - 1] = bound->recevier();

            Object* method = bound->method();

            return this->invokeCall(method, argCount);
        }
        case ObjectType::CLASS:
        {
            ObjectClass* klass = callee.asClass();
            ObjectInstance* instance = allocator->allocateInstance(klass);
            this->_stackTop[-argCount - 1] = Value(instance);
            
            // try to call init
            Value method;
            if (klass->findInitMethod(method))
                return this->invokeCall(method.object(), argCount);

            // no init, but stil pass arguments
            else if (argCount != 0)
                this->runtimeError( utils::format("Expected 0 arguments but got %d.", argCount) );

            return true;
        }
        case ObjectType::CLOSURE:
            return this->call(callee.asClosure(), argCount);
        case ObjectType::NATIVE:
        {
            Value result = callee.asNative()->execute(argCount, this->_stackTop - argCount);
            this->_stackTop -= (argCount + 1);
            this->push(result);
            return true;
        }
        default: break;
        }
    }

    return false;
}

//! \brief create a new frames to call a closure object with specify arguments
//! \param[in] closure closure object
//! \param[in] argCount arguments size
//! \note the function with check the 'argCount'
bool VirtualMachine::call(ObjectClosure* closure, int argCount)
{
    // check argument count
    ObjectFunction* function = closure->function();
    if (argCount != function->arity())
        this->runtimeError("Expected  function arguments");

    if (this->_frames.size() == FRAMES_MAX)
        this->runtimeError("Stack overflow.");

    this->_frames.push_back(CallFrame(
        // arguments compiled as the argument at the begining of a function
        // and before call a function, it argument already been put on the stack top
        // so make the new function's stack as this->_stackTop - argCount - 1 can meet the argument list
        // etc. [ <script> ][ <fn main> ][1][2][3][], then main's stack point to  [ <fn main> ] !
        closure, function->chunk().data(), this->_stackTop - argCount - 1
    ));

    return true;
}

bool VirtualMachine::invokeCall(Object* callee, int argCount)
{
    if (callee->isClosure())
        return this->call( callee->asClosure(), argCount );
    else if (callee->isNative())
    {
        Value result = callee->asNative()->execute(argCount + 1, this->_stackTop - argCount - 1);
        this->_stackTop -= (argCount + 1);
        this->push(result);
        return true;
    }

    return false;
}

//! \brief try invoke a method
void VirtualMachine::tryInvoke(ObjectString* name, int argCount)
{
    // get the object ( object.fun()  )
    Value receiver = this->peek(argCount);
    if (!receiver.isInstance())
        this->runtimeError("Only instances have methods.");

    ObjectInstance* instance = receiver.asInstance();

    // find the method name in instance's fields
    // because maybe the fiels is also a function!
    if (!instance->isNative())
    {
        Value filed;
        if (instance->findField(name, filed))
        {
            this->_stackTop[-argCount - 1] = filed;
            this->callValue(filed, argCount);
            return;
        }
    }

    // no found, try to find it in it's class's _methods
    this->invokeFromClass(instance->klass(), name, argCount);
}

//! \brief try to find the mrthod 'name' in klass's _methods, and call it
//! \param[in] klass the klass object
//! \param[in] name the method's name
//! \param[in] argCount the method's argCount
void VirtualMachine::invokeFromClass(ObjectClass* klass, ObjectString* name, int argCount)
{
    Value method;
    if (!klass->findMethod(name, method))
        this->runtimeError( utils::format("Undefined property '%s'.", name->cstr()) );

    this->invokeCall(method.object(), argCount);
}

//! \brief try to find a UpvalueObject which manage the specify Value* in _openUpvalues list
//! \brief if find return it, or create a new UpvalueObject and link it
//! \param[in] local the Value* need to found
//! \return ObjectUpvalue*
ObjectUpvalue* VirtualMachine::captureUpvalue(Value* local)
{
    ObjectUpvalue* prevUpvalue = nullptr;
    ObjectUpvalue* upvalue = this->_openUpvalues;

    // loop for the upvalue list, end the while because:
    // 1. The local slot we stopped at is the slot we’re looking for.
    // 2. We ran out of upvalues to find.
    // 3. We found an upvalue whose local slot is below the one we’re looking for.
    while (upvalue != nullptr && upvalue->location() > local)
    {
        prevUpvalue = upvalue;
        upvalue = upvalue->next();
    }

    // if find!
    if (upvalue != nullptr && upvalue->location() == local)
        return upvalue;

    ObjectUpvalue* createUpvalue = allocator->allocateUpvalue(local);
    createUpvalue->setNext(upvalue);

    // link createUpvalue
    if (prevUpvalue == nullptr)
        this->_openUpvalues = createUpvalue;
    else
        prevUpvalue->setNext(createUpvalue);
    
    return createUpvalue;
}

//! \brief 'close' the upvalue variable 
void VirtualMachine::closeUpvalues(Value* last) noexcept
{
    while (this->_openUpvalues != nullptr && this->_openUpvalues->location() >= last)
    {
        ObjectUpvalue* upvalue = this->_openUpvalues;
        upvalue->setClosed(*(upvalue->location()));
        upvalue->setLocation(&(upvalue->closed()));
        this->_openUpvalues = upvalue->next();
    }
}

//! \brief define a native function
void VirtualMachine::defineNative(const char* name, NativeFunction function)
{
    this->push(Value(allocator->allocateString(name, strlen(name))));
    this->push(Value(allocator->allocateNative(function)));
    this->_globals.insert({
        this->_stack[0].asString(), this->_stack[1]
    });
    this->pop();
    this->pop();
}

//! \brief define a method
void VirtualMachine::defineMethod(ObjectString* name)
{
    // stack belike: [ class ][ method closure ]
    Value method = peek(0);
    ObjectClass* klass = this->peek(1).asClass();
    klass->addMethod(name, method);

    // pop the [ method closure ]
    this->pop();
}

//! \brief try to find the method 'name' in klass's method table
//! \brief if so, it push the method into the stack
void VirtualMachine::tryBindMethod(ObjectClass* klass, ObjectString* name)
{
    Value method;
    if (!klass->findMethod(name, method))
        this->runtimeError(utils::format("Undefined property and method '%s'.", name->cstr()));

    // if the name is really a method in klass
    // build a boundMethod, bound the instance and the method!
    ObjectBoundMethod* bound = allocator->allocateBoundMethod(this->peek(0), method.object());
    this->pop();
    this->push(Value(bound));
}

}